﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml.Linq;
using Dycox.ComponentModel;
using System.ComponentModel;
using System.Xml;
using System.Xml.Schema;

namespace Dycox.Workflows
{
    public class XmlWorkflowInitializer
    {
        public static readonly XNamespace WorkflowNS = "urn:effun-net-dycox-workflow-conf";

        Workflow _instance;

        private XmlWorkflowInitializer(Workflow instance)
        {
            if (instance == null)
                throw new ArgumentNullException("instance");

            _instance = instance;
        }

        private static Stream GetXmlStream(Workflow instance, string resName)
        {
            Stream stream;

            if (resName == null)
                resName = instance.GetType().FullName + ".xml";
            
            stream = instance.GetType().Assembly.GetManifestResourceStream(resName);

            if (stream == null)
                throw new InvalidOperationException("Cannot retrieve XML stream from manifest.");

            return stream;
        }

        private static XmlReader CreateReader(Stream input)
        {
            var settings = Dycox.Xml.XmlSchemaLoader.CreateXmlReaderSettings(WorkflowNS.NamespaceName);

            return XmlReader.Create(input, settings);
        }

        private void Load(Stream input)
        {
            if (input == null)
                throw new ArgumentNullException("input");

            lock (_instance)
            {
                using (XmlReader reader = CreateReader(input))
                {
                    XDocument doc;

                    try
                    {
                        doc = XDocument.Load(reader);
                    }
                    catch(System.Xml.Schema.XmlSchemaValidationException vex)
                    {
                        throw new WorkflowException("Invalid workflow configuration file.", vex);
                    }

                    ((ISupportInitialize)_instance).BeginInit();

                    try
                    {
                        foreach (XElement elt in doc.Element(WorkflowNS + "workflow").Elements())
                        {
                            if (elt.Name.Namespace == WorkflowNS)
                            {
                                string tag = elt.Name.LocalName;

                                switch (tag)
                                {
                                    case "settings":
                                        ReadSettings(elt);
                                        break;
                                    case "stages":
                                        ReadStages(elt);
                                        break;
                                }
                            }
                        }
                    }
                    finally
                    {
                        ((ISupportInitialize)_instance).EndInit();
                    }
                }
            }
        }

        public static void Load(Workflow instance)
        {
            Load(instance, (string)null);
        }

        public static void Load(Workflow instance, string resName)
        {
            if (instance == null)
                throw new ArgumentNullException("instance");

            XmlWorkflowInitializer loader = new XmlWorkflowInitializer(instance);

            using (Stream stream = GetXmlStream(instance, resName))
            {
                Load(instance, stream);
            }
        }

        public static void Load(Workflow instance, Stream input)
        {
            if (instance == null)
                throw new ArgumentNullException("instance");
            if (input == null)
                throw new ArgumentNullException("input");

            XmlWorkflowInitializer loader = new XmlWorkflowInitializer(instance);
            loader.Load(input);
        }

        private void ReadSettings(XElement xeSettings)
        {
            _instance.DisplayName = xeSettings.GetAttrString("displayName");
            _instance.Description = xeSettings.GetAttrString("description");

            string value = xeSettings.GetAttrString("currentVersion");
            if (!string.IsNullOrEmpty(value))
                _instance.CurrentVersion = Version.Parse(value);

            if ((value = (string)xeSettings.Attribute("compatibleVersion")) != null)
                _instance.CompatibleVersion = new Version(value);

            _instance.ProcessStartMode = xeSettings.GetAttrEnum<ActivationMode>("processStartMode");

            XElement xeLocks = xeSettings.Element(WorkflowNS + "threadLocks");
            if (xeLocks != null)
            {
                foreach (XElement xel in xeLocks.Elements(WorkflowNS + "lock"))
                {
                    _instance.ThreadLocks.Add(new ThreadLock((string)xel.Attribute("name"), (string)xel.Attribute("description")));
                }
            }

            xeLocks = xeSettings.Element(WorkflowNS + "completeConditions");
            if (xeLocks != null)
                ReadConditions(xeLocks, _instance.CompleteConditions);
        }

        private void ReadHints(XElement xeHints, HintCollection coll)
        {
            if (xeHints != null)
            {
                foreach (XElement xeh in xeHints.Elements(WorkflowNS + "hint"))
                {
                    Hint hint = new Hint();
                    hint.Text = (string)xeh.Attribute("text");
                    hint.ShortText = (string)xeh.Attribute("short");
                    hint.Kind = xeh.GetAttrEnum<MessageKind>("kind", MessageKind.Information);

                    XElement xec = xeh.Element(WorkflowNS + "conditions");
                    if (xec != null)
                        ReadConditions(xec, hint.Conditions);

                    coll.Add(hint);
                }
            }
        }

        private void ReadTransitions(Stage stage, XElement element)
        {

            if (element != null)
            {
                foreach (var xeTo in element.Elements())
                {
                    XName name = xeTo.Name;
                    XElement xet;
                    if (name.Namespace == WorkflowNS)
                    {
                        Transition t;

                        switch (name.LocalName)
                        {
                            case "to":
                                t = new Transition(stage, _instance.Stages[xeTo.GetAttrString("stage")]);
                                xet = xeTo.Element(WorkflowNS + "where");
                                if (xet != null)
                                {
                                    XElement elt = xet.Element(WorkflowNS + "user-selected");
                                    if (elt != null)
                                    {
                                        var tc = CreateTransitionCondition(elt);
                                        if (tc != null) t.Conditions.Add(tc);
                                    }
                                    else if ((elt = xet.Element(WorkflowNS + "conditions")) != null)
                                    {
                                        ReadConditions(elt, t.Conditions);
                                    }
                                    else if ((elt = xet.Element(WorkflowNS + "condition")) != null)
                                    {
                                        throw new NotSupportedException("The expresional condition is not supported yet.");
                                        //t.Conditions.Add(new Conditions.ExpressionalCondition(elt.GetAttrString("expression")));
                                    }
                                }
                                break;
                            case "no-more-step":
                                t = new Transition(stage, _instance.NoMoreStep);
                                break;
                            case "exit":
                                t = new Transition(stage, _instance.AnyStage);
                                break;
                            case "enter":
                                t = new Transition(_instance.AnyStage, stage);
                                if ((xet = xeTo.Element(WorkflowNS + "conditions")) != null)
                                {
                                    stage.ProhibitMessage = (string)xet.Attribute("errorMessage");
                                    ReadConditions(xet, t.Conditions);
                                }
                                break;
                            default:
                                throw new InvalidOperationException("Unexpected element " + name.LocalName + " found.");
                        }


                        if ((xet = xeTo.Element(WorkflowNS + "operations")) != null)
                            ReadOperations(xet, t.Operations);

                        t.Rank = ((int?)xeTo.Attribute("rank")).GetValueOrDefault();
                        t.StepMarshalling = xeTo.GetAttrEnum<StepMarshalling>("marshal", StepMarshalling.Default);
                        t.StepActivation = xeTo.GetAttrEnum<ActivationMode>("stepActivation");

                        //if (t.Operations.Count + t.Conditions.Count > 0 || t.Rank != 0)
                            _instance.Transitions.Add(t);
                    }
                }

            }
        }

        private void ReadConditions(XElement element, ConditionCollection coll)
        {
            coll.ItemsRelation = element.GetAttrEnum<ConditionRelation>("relation");

            foreach (var elt in element.Elements())
            {
                XName name = elt.Name;
                if (name.Namespace == WorkflowNS)
                {
                    if (name.LocalName == "conditions")
                    {
                        ConditionCollection coll1;

                        if (coll is TransitionConditionCollection)
                            coll1 = new TransitionConditionCollection(((TransitionConditionCollection)coll).Transition);
                        else
                            coll1 = new ConditionCollection();

                        Conditions.ConditionCollectionCondition ccc = new Conditions.ConditionCollectionCondition(coll1);
                        ReadConditions(elt, ccc.Collection);
                        coll.Add(ccc);
                    }
                    else
                    {
                        var tc = CreateTransitionCondition(elt);
                        if (tc != null) coll.Add(tc);
                    }
                }
            }


        }

        static Dictionary<string, Type> ConditionTypeMap = new Dictionary<string, Type> {
            { "compare", typeof(Conditions.CompareCondition)},
            { "range", typeof(Conditions.RangeCondition)},
            { "regex", typeof(Conditions.RegexCondition)},
            { "user-selected", typeof(Conditions.UserSelectedCondition)},
            { "custom", typeof(Conditions.CustomCondition)}
        };

        private TransitionCondition CreateTransitionCondition(XElement elt)
        {
            XName name = elt.Name;

            Type type;

            if (ConditionTypeMap.TryGetValue(name.LocalName, out type))
            {
                Dictionary<string, string> parameters = new Dictionary<string, string>();
                foreach (XAttribute attr in elt.Attributes())
                {
                    name = attr.Name;
                    if (string.IsNullOrEmpty(name.NamespaceName) || name.Namespace == WorkflowNS)
                    {
                        parameters[name.LocalName] = attr.Value;
                    }
                }

                object cond = Activator.CreateInstance(type, parameters);
                return (TransitionCondition)cond;
            }
            else
                return null;
        }

        private void ReadStages(XElement element)
        {
            List<ReadStageTargetsState> targets = new List<ReadStageTargetsState>();

            foreach (var xeStage in element.Elements())
            {
                XName xname = xeStage.Name;

                if (xname.Namespace == WorkflowNS)
                {
                    Stage stage;
                    StageUsage usage;
                    string name = xeStage.GetAttrString("name");
                    string typeName = (string)xeStage.Attribute("type");
                    Type type = typeof(Stage);

                    switch (xname.LocalName)
                    {
                        case "entrance":
                            usage = StageUsage.Entrance;
                            break;
                        case "stage":
                            usage = StageUsage.Normal;
                            break;
                        case "injection":
                            usage = StageUsage.Injection;
                            break;
                        default:
                            throw new InvalidOperationException("Invalid stage node: " + xname);
                    }

                    if (!string.IsNullOrEmpty(typeName))
                    {
                        type = this.GetType(typeName);
                        if (!type.IsSubclassOf(typeof(Stage)) || type.IsAbstract)
                            throw new InvalidOperationException("Stage type " + typeName + " is not valid.");
                    }

                    stage = (Stage)Activator.CreateInstance(type, name);

                    ((ISupportInitialize)stage).BeginInit();

                    stage.Usage = usage;
                    stage.ContractInfo = ReadContract(xeStage.Element(WorkflowNS + "contract"));
                    stage.Class = xeStage.GetAttrEnum<StageClass>("class", StageClass.Major);

                    name = (string)xeStage.Attribute("displayName");
                    if (name != null)
                        stage.DisplayName = name;

                    if (usage == StageUsage.Injection)
                        stage.InjectionMode = xeStage.GetAttrEnum<InjectionMode>("mode", InjectionMode.Process);

                    ((ISupportInitialize)stage).EndInit();
                    _instance.Stages.Add(stage);

                    XElement xet = xeStage.Element(WorkflowNS + "transition");
                    if (xet != null)
                        targets.Add(new ReadStageTargetsState { Stage = stage, Element = xet });

                    ReadStageProperties(xeStage.Element(WorkflowNS + "properties"), stage.Properties);

                    if ((xet = xeStage.Element(WorkflowNS + "hints")) != null)
                        ReadHints(xet, stage.Hints);
                }
            }

            foreach (var state in targets)
            {
                ReadTransitions(state.Stage, state.Element);
            }

        }

        private void ReadStageProperties(XElement xeProperties, StageProperties properties)
        {
            if (xeProperties != null)
            {
                foreach (XElement xep in xeProperties.Elements(WorkflowNS + "property"))
                {
                    string name = (string)xep.Attribute("name");
                    string text = (string)xep.Attribute("value");
                    string typeName = (string)xep.Attribute("type");

                    if (string.IsNullOrEmpty(typeName))
                    {
                        properties[name] = text;
                    }
                    else
                    {
                        object value;
                        Type type = GetType(typeName);

                        if (type != typeof(string))
                            value = Dycox.Utils.ConvertUtil.ConvertType(text, type);
                        else
                            value = text;

                        properties[name] = value;
                    }
                }
            }
        }

        private ContractInfo ReadContract(XElement xeContract)
        {
            if (xeContract == null)
                return null;

            string typeName = (string)xeContract.Attribute("type");
            Type type;

            if (!string.IsNullOrEmpty(typeName))
            {
                if (typeName == "default")
                    type = typeof(StageOperationContract);
                else
                {

                    type = _instance.GetType().Assembly.GetType(typeName, false);
                    if (type == null)
                        type = Type.GetType(typeName, false);
                    if (type == null)
                        throw new InvalidOperationException(SR.GetString("type_not_found", typeName));
                }
            }
            else
            {
                var melt = xeContract.Element(ModelView.ModelNamespace + "model");
                if (melt != null)
                    type = GuessModelType(melt);
                else
                    type = null;
            }

            if (type != null)
            {
                ContractInfo info = new ContractInfo(type);

                XElement xeInit = xeContract.Element(WorkflowNS + "initialization");
                if (xeInit != null)
                {
                    foreach (var xe in xeInit.Elements(WorkflowNS + "property"))
                    {
                        info.PropertyValues[(string)xe.Attribute("name")] = (string)xe.Attribute("value");
                    }
                }

                return info;
            }
            else
                return null;
        }

        private Type GuessModelType(XElement xeModel)
        {
            XNamespace m = ModelView.ModelNamespace;
            string namespce, name;
            Type type;

            name = (string)xeModel.Attribute("name");
            namespce = (string)xeModel.Attribute("namespace");
            if (string.IsNullOrEmpty(namespce))
            {
                XElement xe = xeModel.Document.Root.Element(m + "fileOptions");
                if (xe != null)
                    namespce = (string)xe.Attribute("defaultNamespace");

                if (string.IsNullOrEmpty(namespce))
                {
                    foreach (XElement xem in xeModel.Document.Root.Elements(m + "model"))
                    {
                        if (xem.Equals(xeModel)) break;
                        string tmp = (string)xem.Attribute("namespace");
                        if (!string.IsNullOrEmpty(tmp)) namespce = tmp;
                    }
                }

            }

            if (string.IsNullOrEmpty(namespce))
                throw new WorkflowException(SR.GetString("guess_type_failure", name));

            name = namespce + Type.Delimiter + name;
            type = _instance.GetType().Assembly.GetType(name);
            if (type == null)
                throw new InvalidOperationException(SR.GetString("type_not_found", name));

            return type;
        }

        private void ReadOperations(XElement xeEvents, TransitionOperationCollection coll)
        {
            if (xeEvents != null)
            {
                foreach (XElement xee in xeEvents.Elements())
                {
                    if (xee.Name.Namespace == WorkflowNS)
                    {
                        TransitionOperation op;

                        switch (xee.Name.LocalName)
                        {
                            case "start-process":
                                op = new StartProcessOperation();
                                break;
                            case "set-lock":
                                op = new ThreadLockSetOperation((string)xee.Attribute("name"));
                                break;
                            case "release-lock":
                                op = new ThreadLockReleaseOperation((string)xee.Attribute("name"));
                                break;
                            case "wait-lock":
                                op = new ThreadLockWaitOperation((string)xee.Attribute("name"));
                                break;
                            case "auto-complete":
                                throw new NotImplementedException();
                            case "abort-process":
                                op = new AbortProcessOperation((string)xee.Attribute("reason"));
                                break;
                            case "suspend-process":
                                op = new SuspendProcessOperation(xee.GetAttrEnum<ProcessSuspendingReasons>("reason"));
                                break;
                            case "resume-process":
                                op = new ResumeProcessOperation(xee.GetAttrEnum<ProcessSuspendingReasons>("reason"));
                                break;
                            case "custom":
                                op = new CustomOperation(CreateObject<ICustomTransitionOperation>((string)xee.Attribute("type")), (string)xee.Attribute("state"));
                                break;
                            default:
                                throw new NotSupportedException("Transition operation " + xee.Name + " not supported.");
                        }

                        var xec = xee.Element(WorkflowNS + "conditions");
                        if (xec != null)
                            ReadConditions(xec, op.Conditions);

                        coll.Add(op);
                    }
                }
            }
        }

        private T CreateObject<T>(string typeName)
        {
            if (typeName == null)
                throw new ArgumentNullException("typeName");
            
            Type type;

            if (typeName.IndexOf(',') > 0)
                type = Type.GetType(typeName, true);
            else
            {
                if (typeName.IndexOf(Type.Delimiter) < 0)
                    typeName = _instance.GetType().Namespace + Type.Delimiter + typeName;

                type = _instance.GetType().Assembly.GetType(typeName, true);
            }

            object obj = Activator.CreateInstance(type);
            if (obj is T) return (T)obj;
            else throw new InvalidCastException(SR.GetString("invalid_cast", obj.GetType(), type));
        }

        private static Dictionary<string, Type> TypeSurrogators = new Dictionary<string, Type>() {
            { "string", typeof(string)},
            { "int", typeof(int)},
            { "short", typeof(short)},
            { "byte", typeof(byte)},
            { "bool", typeof(bool)},
            { "float", typeof(float)},
            { "double", typeof(double)},
            { "decimal", typeof(decimal)},
            { "uint", typeof(uint)},
            { "ulong", typeof(ulong)},
            { "ushort", typeof(ushort)},
            { "sbyte", typeof(sbyte)},
            { "char", typeof(char)}
        };

        private Type GetType(string typeName)
        {
            Type type;

            if (!TypeSurrogators.TryGetValue(typeName, out type))
            {
                if (typeName.IndexOf(Type.Delimiter) < 0)
                    typeName = "System." + typeName;
                type = Type.GetType(typeName, false);

                if (type == null)
                    type = _instance.GetType().Assembly.GetType(typeName, true, false);
            }

            return type;
        }

        class ReadStageTargetsState
        {
            public Stage Stage;
            public XElement Element;
        }
    }
}
